// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using EnsureThat;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Extensions.Logging;
using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag;
using Microsoft.Health.Dicom.Core.Features.Model;
using Microsoft.Health.Dicom.Operations.Extensions;
using Microsoft.Health.Dicom.Operations.Indexing.Models;

namespace Microsoft.Health.Dicom.Operations.Indexing
{
    public partial class ReindexDurableFunction
    {
        /// <summary>
        /// Asynchronously creates an index for the provided query tags over the previously added data.
        /// </summary>
        /// <remarks>
        /// Durable functions are reliable, and their implementations will be executed repeatedly over the lifetime of
        /// a single instance.
        /// </remarks>
        /// <param name="context">The context for the orchestration instance.</param>
        /// <param name="logger">A diagnostic logger.</param>
        /// <returns>A task representing the <see cref="ReindexInstancesAsync"/> operation.</returns>
        [FunctionName(nameof(ReindexInstancesAsync))]
        public async Task ReindexInstancesAsync(
            [OrchestrationTrigger] IDurableOrchestrationContext context,
            ILogger logger)
        {
            EnsureArg.IsNotNull(context, nameof(context));

            logger = context.CreateReplaySafeLogger(logger);
            ReindexInput input = context.GetInput<ReindexInput>();

            // The ID should be a GUID as generated by the trigger, but we'll assert here just to make sure!
            if (!context.HasInstanceGuid())
            {
                return;
            }

            // Fetch the set of query tags that require re-indexing
            IReadOnlyList<ExtendedQueryTagStoreEntry> queryTags = await GetOperationQueryTagsAsync(context, input);
            logger.LogInformation(
                "Found {Count} extended query tag paths to re-index {{{TagPaths}}}.",
                queryTags.Count,
                string.Join(", ", queryTags.Select(x => x.Path)));

            List<int> queryTagKeys = queryTags.Select(x => x.Key).ToList();
            if (queryTags.Count > 0)
            {
                IReadOnlyList<WatermarkRange> batches = await context.CallActivityWithRetryAsync<IReadOnlyList<WatermarkRange>>(
                    nameof(GetInstanceBatchesAsync),
                    _options.ActivityRetryOptions,
                    input.Completed?.Start - 1);

                if (batches.Count > 0)
                {
                    // Note that batches are in reverse order because we start from the highest watermark
                    var batchRange = new WatermarkRange(batches[^1].Start, batches[0].End);

                    logger.LogInformation("Beginning to re-index the range {Range}.", batchRange);
                    await Task.WhenAll(batches
                        .Select(x => context.CallActivityWithRetryAsync(
                            nameof(ReindexBatchAsync),
                            _options.ActivityRetryOptions,
                            new ReindexBatch { QueryTags = queryTags, WatermarkRange = x })));

                    // Create a new orchestration with the same instance ID to process the remaining data
                    logger.LogInformation("Completed re-indexing the range {Range}. Continuing with new execution...", batchRange);

                    WatermarkRange completed = input.Completed.HasValue
                        ? new WatermarkRange(batchRange.Start, input.Completed.Value.End)
                        : batchRange;

                    context.ContinueAsNew(
                        new ReindexInput
                        {
                            QueryTagKeys = queryTagKeys,
                            Completed = completed,
                        });
                }
                else
                {
                    IReadOnlyList<int> completed = await context.CallActivityWithRetryAsync<IReadOnlyList<int>>(
                        nameof(CompleteReindexingAsync),
                        _options.ActivityRetryOptions,
                        queryTagKeys);

                    logger.LogInformation(
                        "Completed re-indexing for the following extended query tags {{{QueryTagKeys}}}.",
                        string.Join(", ", completed));
                }
            }
            else
            {
                logger.LogWarning(
                    "Could not find any query tags for the re-indexing operation '{OperationId}'.",
                    context.InstanceId);
            }
        }

        // Determine the set of query tags that should be indexed and only continue if there is at least 1.
        // For the first time this orchestration executes, assign all of the tags in the input to the operation,
        // otherwise simply fetch the tags from the database for this operation.
        private Task<IReadOnlyList<ExtendedQueryTagStoreEntry>> GetOperationQueryTagsAsync(IDurableOrchestrationContext context, ReindexInput input)
            => input.Completed.HasValue
                ? context.CallActivityWithRetryAsync<IReadOnlyList<ExtendedQueryTagStoreEntry>>(
                    nameof(GetQueryTagsAsync),
                    _options.ActivityRetryOptions,
                    null)
                : context.CallActivityWithRetryAsync<IReadOnlyList<ExtendedQueryTagStoreEntry>>(
                    nameof(AssignReindexingOperationAsync),
                    _options.ActivityRetryOptions,
                    input.QueryTagKeys);

        private static int GetPercentComplete(WatermarkRange range)
        {
            // If we processed a batch, there must be at least one row. And because the Watermark
            // sequence starts at 1, we know both Start and End must at least be 1.
            return range.End == 1 ? 100 : (int)((double)(range.End - range.Start + 1) / range.End * 100);
        }
    }
}
